package jp.co.nes.awssupport.common.utility;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.ss.util.CellRangeAddress;

/**
 * POI関連ユーティリティです。
 */
public class PoiUtil {

    /**
     * デフォルトコンストラクタです。<br>
     * 本クラスはインスタンスを生成しません。
     */
    private PoiUtil() {
    }

    /**
     * シートから指定された内容が含まれている最初のRowとCellを返却します。<br>
     * なお、該当するセルが存在しない場合は、nullを返却します。
     *
     * @param sheet シート
     * @param value セルに含まれている内容
     * @return RowCellオブジェクト
     */
    public static Map.Entry<Row, Cell> getFirstRowCell(Sheet sheet, String value) {
        Map<Row, Cell> rowCellMap = new HashMap<>();
        rowLoop: for (Iterator<Row> rowIterator = sheet.iterator(); rowIterator.hasNext();) {
            Row row = rowIterator.next();
            for (Iterator<Cell> cellIterator = row.iterator(); cellIterator.hasNext();) {
                Cell cell = cellIterator.next();

                if (cell.getCellType() != Cell.CELL_TYPE_STRING) {
                    continue;
                }

                String cellValue = cell.getStringCellValue();

                if (cellValue != null && cellValue.equals(value)) {
                    rowCellMap.put(row, cell);
                    break rowLoop;
                }
            }
        }

        Map.Entry<Row, Cell> result = null;
        if (!rowCellMap.isEmpty()) {
            for (Map.Entry<Row, Cell> entry : rowCellMap.entrySet()) {
                result = entry;
                break;
            }
        }
        return result;
    }

    /**
     * 引数で指定された行に関するセル結合情報を返却します。<br>
     * 返却するマップには、キーに結合されているセルの左上の列、値には、右下の列が設定されています。
     *
     * @param sheet シート
     * @param rowNum 行
     * @return セル結合情報
     */
    public static Map<Integer, Integer> getMergedMap(Sheet sheet, int rowNum) {
        Map<Integer, Integer> mergedMap = new HashMap<>();
        Row row = sheet.getRow(rowNum);
        int lastCellNum = row.getLastCellNum();
        for (int i = 0; i < sheet.getNumMergedRegions(); i++) {
            CellRangeAddress cellRangeAddress = sheet.getMergedRegion(i);
            for (int j = 0; j < lastCellNum; j++) {
                if (cellRangeAddress.isInRange(rowNum, j)) {
                    int firstColumn = cellRangeAddress.getFirstColumn();
                    if (!mergedMap.containsKey(firstColumn)) {
                        mergedMap.put(firstColumn, cellRangeAddress.getLastColumn());
                    }
                }
            }
        }
        return mergedMap;
    }

    /**
     * 引数で指定されたRowの下に新規行を挿入します。
     *
     * @param sheet シート
     * @param sourceRow 新規行の高さ、スタイルを設定する際に使用するRow
     * @param mergedMap 新規行を結合する際に使用するセル結合情報(キーに結合されているセルの左上の列、値には、右下の列を設定)
     */
    public static void insertRow(Sheet sheet, Row sourceRow, Map<Integer, Integer> mergedMap) {
        // 行挿入は、shiftRowsで行を1行下にシフトし、その後、createRowする。
        // そして、createRowした行に対して、高さ、スタイル、セル結合の設定を実施する。

        // 行をシフトさせる
        int startShiftRowNum = sourceRow.getRowNum() + 1;
        int endShiftRowNum = sheet.getLastRowNum();
        sheet.shiftRows(startShiftRowNum, endShiftRowNum, 1, true, false);

        Row newRow = sheet.createRow(startShiftRowNum);

        // 新規行の高さを設定する。
        newRow.setHeight(sourceRow.getHeight());

        // 新規行のスタイルを設定する。
        int lastCellNum = sourceRow.getLastCellNum();
        for (int i = 0; i < lastCellNum; i++) {
            Cell sourceCell = sourceRow.getCell(i);
            Cell newCell = newRow.createCell(i);
            newCell.setCellStyle(sourceCell.getCellStyle());
        }

        if (mergedMap != null) {
            // 新規行のセルを結合する
            mergedMap.entrySet().stream()
                    .forEach(entry -> merged(sheet, startShiftRowNum, entry.getKey(), entry.getValue()));
        }
    }

    /**
     * 行を削除します。
     *
     * @param sheet シート
     * @param delRow 削除するRow
     * @param mergedMap 削除時に結合解除するセル結合情報(キーに結合されているセルの左上の列、値には、右下の列を設定)
     */
    public static void deleteRow(Sheet sheet, Row delRow, Map<Integer, Integer> mergedMap) {
        // 念のため、removeRowで行のデータを削除する。
        // なお、removeRowはデータを削除するだけで、行は削除しない。
        // 行を削除するためには、行をシフトさせる必要がある。
        sheet.removeRow(delRow);

        int delRowNum = delRow.getRowNum();

        if (mergedMap != null) {
            // 行をシフトさせ、削除する予定の行をセル結合を解除する(解除しないと行をシフトさせる際、例外が発生する)。
            mergedMap.entrySet().stream().forEach(entry -> removeMerged(sheet, delRowNum, entry.getKey()));
        }

        // 削除する行の1行下と最終行の設定
        int startShiftRowNum = delRowNum + 1;
        int endShiftRowNum = sheet.getLastRowNum();

        // 削除する行の1行下から最終行までのRowをリストに追加
        List<Row> rowList = new ArrayList<>();
        for (int i = startShiftRowNum; i <= endShiftRowNum; i++) {
            rowList.add(sheet.getRow(i));
        }

        // 削除する行の1行下から最終行までを、1行上にシフトさせる。
        sheet.shiftRows(startShiftRowNum, endShiftRowNum, -1);

        // シフト後、高さが変わっている行も存在するので、保持していたリストの情報で高さを設定
        for (int i = 0; i < rowList.size(); i++) {
            Row row = sheet.getRow(delRowNum + i);
            row.setHeight(rowList.get(i).getHeight());
        }
    }

    /**
     * 指定されたセルを結合します。
     *
     * @param sheet シート
     * @param rowNum 結合するセルの行
     * @param firstCol 結合するセルの開始列
     * @param lastCol 結合するセルの終了列
     */
    public static void merged(Sheet sheet, int rowNum, int firstColNum, int lastColNum) {
        CellRangeAddress newMaerged = new CellRangeAddress(rowNum, rowNum, firstColNum, lastColNum);
        sheet.addMergedRegion(newMaerged);
    }

    /**
     * 指定されたセルの結合を解除します。
     *
     * @param sheet シート
     * @param rowNum 結合を解除するセルの行
     * @param colNum 結合を解除するセルの列
     */
    public static void removeMerged(Sheet sheet, int rowNum, int colNum) {
        for (int i = 0; i < sheet.getNumMergedRegions(); i++) {
            CellRangeAddress cellRangeAddress = sheet.getMergedRegion(i);
            if (cellRangeAddress.isInRange(rowNum, colNum)) {
                sheet.removeMergedRegion(i);
            }
        }
    }
}
